Skip to content

Conversation

@overheadhunter
Copy link
Member

@overheadhunter overheadhunter commented Jan 27, 2026

This is the website counterpart of https://github.com/cryptomator/api/pull/12.

It adds the new page /hub/register, which has a four step process:

  1. enter email address
  2. review and accept license terms
  3. verify email address (which will create a CE billing record in the backend)
  4. invoke POST /licenses/hub/refresh endpoint to trade in trial license for new CE license

Note

In order to start this workflow, the old trial license must be passed to the site via the url fragment, e.g. /hub/register#oldLicense=ey...

This is just the bare minimum, I imagine to improve this workflow by integrating it with Hub via automatic redirect.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new /hub/register page for registering a Cryptomator Hub instance for a Community Edition license, including a multi-step UI flow, supporting JS logic, and new i18n strings.

Changes:

  • Introduces a new Hub CE registration page layout with a step-based AlpineJS UI and Altcha captcha integration.
  • Adds new frontend logic (hubce.js) to drive the registration/verification/license-fetch flow.
  • Adds page content entries for EN/DE and new EN translations for the CE registration flow.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
layouts/hub-register/single.html New multi-step registration UI, captcha widgets, and script includes for the Hub CE flow
assets/js/hubce.js New client-side flow logic: email confirmation request + license refresh call
i18n/en.yaml Adds English strings for the new Hub CE registration UI
content/hub-register.en.html Registers the EN page route/type for /hub/register
content/hub-register.de.html Registers the DE page route/type for /de/hub/register

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +71 to +72
<form x-ref="form" class="md:col-span-2" @submit.prevent="hubCE.submit(); $refs.captcha.reset()">
<!-- Step 1: Email Address -->
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pressing Enter in the email field (step 1) will submit the form, but $refs.captcha isn’t rendered in that step; calling $refs.captcha.reset() will throw and break the flow. Also, Enter bypasses the disabled “Next” button guard for oldLicense, allowing progression without the required token. Consider guarding the reset (e.g., only when the ref exists) and enforcing oldLicense in validation / making the step-1 navigation button type="button" so the form can’t submit in step 1.

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +115
<button :disabled="feedbackData.inProgress || !acceptTerms || captchaState == 'verifying'" type="submit" class="btn btn-primary w-full md:w-64" data-umami-event="hub-managed-form" x-cloak>
<i :class="{'fa-paper-plane': !feedbackData.inProgress, 'fa-spinner fa-spin': feedbackData.inProgress}" class="fa-solid" aria-hidden="true"></i>
{{ i18n "hub_managed_step_4_submit" }}
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This submit button still uses Hub Managed copy/analytics (hub_managed_step_4_submit, hub-managed-form) on the CE registration flow. That will show the wrong label (and mix Umami events). Add CE-specific i18n/event names or reuse the CE ..._steps_next/submit wording consistently.

Suggested change
<button :disabled="feedbackData.inProgress || !acceptTerms || captchaState == 'verifying'" type="submit" class="btn btn-primary w-full md:w-64" data-umami-event="hub-managed-form" x-cloak>
<i :class="{'fa-paper-plane': !feedbackData.inProgress, 'fa-spinner fa-spin': feedbackData.inProgress}" class="fa-solid" aria-hidden="true"></i>
{{ i18n "hub_managed_step_4_submit" }}
<button :disabled="feedbackData.inProgress || !acceptTerms || captchaState == 'verifying'" type="submit" class="btn btn-primary w-full md:w-64" data-umami-event="hub-ce-registration-step-2-submit" x-cloak>
<i :class="{'fa-paper-plane': !feedbackData.inProgress, 'fa-spinner fa-spin': feedbackData.inProgress}" class="fa-solid" aria-hidden="true"></i>
{{ i18n "hub_ce_registration_steps_submit" }}

Copilot uses AI. Check for mistakes.
Comment on lines +134 to +159
<p>Todo: Please check your e-mails.</p>
</div>
</div>
</template>

<!-- Step 3b: License Key (success) -->
<template x-if="feedbackData.currentStep == 2 &amp;&amp; feedbackData.success">
<div class="md:col-span-2 grid grid-cols-6 gap-6">
<div class="flex flex-col col-span-6 lg:col-span-4">
<p class="hidden md:block font-p text-sm text-gray-500 mb-2">
{{ i18n "hub_managed_steps_title" | safeHTML }}
</p>
<h2 class="font-h2 mb-6">
{{ i18n "hub_ce_registration_step_3_license_title" }}
</h2>
<p>Todo: return URL</p>
{{ $challengeUrl := printf "%s/licenses/hub/challenge" .Site.Params.apiBaseUrl }}
{{ partial "captcha.html" (dict "challengeUrl" $challengeUrl "captchaPayload" "submitData.captcha" "captchaState" "captchaState") }}
<textarea class="block input-box w-full h-48 mb-8" x-text="feedbackData.licenseText" readonly></textarea>

<div class="mt-auto">
<p :class="{'hidden': !feedbackData.errorMessage}" class="text-sm text-red-600 mb-2" x-text="feedbackData.errorMessage"></p>
<button class="btn btn-primary w-full md:w-64" data-umami-event="hub-ce-registration-step-1">
<i :class="{'fa-chevron-right': !feedbackData.inProgress, 'fa-spinner fa-spin': feedbackData.inProgress}" class="fa-solid" aria-hidden="true"></i>
Todo: Return to Hub
</button>
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are user-facing placeholder strings (Todo: ...) and a “Return to Hub” button that currently lives inside the form and will submit the form (triggering hubCE.submit()), not actually return anywhere. These placeholders/actions should be implemented (or removed) before merging to avoid shipping incomplete UX and unintended behavior.

Copilot uses AI. Check for mistakes.
<p>Todo: return URL</p>
{{ $challengeUrl := printf "%s/licenses/hub/challenge" .Site.Params.apiBaseUrl }}
{{ partial "captcha.html" (dict "challengeUrl" $challengeUrl "captchaPayload" "submitData.captcha" "captchaState" "captchaState") }}
<textarea class="block input-box w-full h-48 mb-8" x-text="feedbackData.licenseText" readonly></textarea>
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<textarea ... x-text="feedbackData.licenseText"> won’t update the textarea’s value reliably (the project generally uses x-model/:value for textareas). Bind the textarea’s value (e.g., x-model to a readonly field or :value) so the license text updates when the AJAX call completes.

Suggested change
<textarea class="block input-box w-full h-48 mb-8" x-text="feedbackData.licenseText" readonly></textarea>
<textarea class="block input-box w-full h-48 mb-8" :value="feedbackData.licenseText" readonly></textarea>

Copilot uses AI. Check for mistakes.
{{ partial "altcha-css.html" . }}
{{ end }}
{{ define "main" }}
<section x-data="{steps: ['{{ i18n "hub_ce_registration_step_1_nav_title" }}', '{{ i18n "hub_ce_registration_step_2_confirmation_nav_title" }}', '{{ i18n "hub_ce_registration_step_3_license_nav_title" }}'], feedbackData: {currentStep: 0, success: false, inProgress: false, errorMessage: '', licenseText: null}, submitData: {captcha: null, oldLicense: '', email: '', acceptNewsletter: false}, acceptTerms: false, hubCE: null, captchaState: null}" x-init="hubCE = new HubCE($refs.form, feedbackData, submitData, new URLSearchParams(location.hash.substring(1)))" class="container py-12">
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CE registration flow is described as 4 steps (email → accept terms → verify email → get license), but the UI steps array only has 3 steps and there’s no dedicated “verify email” step in the progress indicator. Either align the UI to the described 4-step process or update the workflow/labels to match the implemented behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +95
getHubLicense() {
$.ajax({
url: REFRESH_LICENSE_URL,
type: 'POST',
data: {
token: this._submitData.oldLicense
}
}).done(response => {
this._feedbackData.licenseText = response;
}).fail(xhr => {
this.onRequestFailed(xhr.responseJSON?.message || 'Fetching license failed.');
});
}
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getHubLicense() doesn’t set inProgress/clear errorMessage, so the UI can’t reliably show loading state or prevent duplicate submits. Also, the template renders an Altcha captcha for the license step, but this request doesn’t send any captcha payload—either remove the captcha from the license step or include its payload in the POST so the widget has an effect.

Copilot uses AI. Check for mistakes.
<div class="grid grid-cols-6 gap-6">
<div class="flex flex-col col-span-6 lg:col-span-4">
<p class="hidden md:block font-p text-sm text-gray-500 mb-2">
{{ i18n "hub_managed_steps_title" | safeHTML }}
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The steps title inside the step content uses hub_managed_steps_title, but this page introduces hub_ce_registration_steps_title. This makes the new CE i18n key unused and risks inconsistent translations between mobile/desktop. Switch these occurrences to the CE-specific key (or remove the CE key if you intend to reuse the managed one).

Suggested change
{{ i18n "hub_managed_steps_title" | safeHTML }}
{{ i18n "hub_ce_registration_steps_title" | safeHTML }}

Copilot uses AI. Check for mistakes.
<script type="text/javascript" src="{{ $hubCeJs.RelPermalink }}" integrity="{{ $hubCeJs.Data.Integrity }}" defer></script>
{{ $altchaJs := resources.Get "js/altcha/altcha.js" | minify | fingerprint }}
<script type="module" src="{{ $altchaJs.RelPermalink }}" integrity="{{ $altchaJs.Data.Integrity }}" defer></script>
{{ $altchaWorkerJs := resources.Get "js/altcha/worker.js" }}
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In production, altchaWorkerJs isn’t minify|fingerprint’d but an integrity attribute is still emitted. This will likely produce an empty/incorrect integrity value and prevents cache-busting. Apply the same minify | fingerprint pipeline as the other scripts (or drop integrity if you intentionally don’t fingerprint).

Suggested change
{{ $altchaWorkerJs := resources.Get "js/altcha/worker.js" }}
{{ $altchaWorkerJs := resources.Get "js/altcha/worker.js" | minify | fingerprint }}

Copilot uses AI. Check for mistakes.
$(this._form).find(':input').addClass('show-invalid');
this._feedbackData.errorMessage = 'Please fill in all required fields.';
return;
}
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateEmail() allows progression even when oldLicense is missing (it’s only blocked by the disabled button in the HTML). Because the form can be submitted via Enter key, this path can advance with a null/empty token and later POST it to the backend. Add an explicit oldLicense check here (and surface a clear error) to match the UI constraint.

Suggested change
}
}
// Ensure oldLicense is present before allowing progression.
const oldLicense = this._submitData.oldLicense;
if (!oldLicense) {
this._feedbackData.inProgress = false;
this._feedbackData.errorMessage = 'License information is missing. Please open this page using the link provided in your email.';
return;
}

Copilot uses AI. Check for mistakes.
const VERIFY_EMAIL_URL = API_BASE_URL + '/connect/email/verify';
const REFRESH_LICENSE_URL = API_BASE_URL + '/licenses/hub/refresh';

class HubCE {
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused class HubCE.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants